iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
AI Engineering

不是模型太慢,是你沒算過這筆帳:地端 LLM 工程實戰 30 天系列 第 23

Day 23 - MTEB 第一名不是你的第一名:embedding 選型與授權地雷

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260903/20183550YnOVbivY4b.png

昨天把一次答錯拆成七層。今天把手伸進第三層:向量本身

我原本只想挑一顆 embedding。沒想到第一關,竟然不是分數,而是授權。

要的是能吃表格、圖表與掃描件的那種,同類裡下載量最高的是 jina-embeddings-v4(HF visual-document-retrieval 分類 467,674 次/月,2026-09-03 查)。

HF 頁面的 license 欄是空的:cardData.licenselicense_name 都回 null,空 tag 不觸發任何規則:掃描器看到 unknown,不是放行就是誤擋。往下讀 model card 的 ## License,它自己說 CC-BY-NC 是誤標。

完整條文藏在 repo 的 LICENSE:Qwen Research License,Section 2(a) 寫「…the Materials FOR NON-COMMERCIAL PURPOSES ONLY」,1(i) 把 Non-Commercial 定義成 research or evaluation。但 2(b) 留了一道門:商用要另外申請。所以精確的結論不是「不能用」,而是拿它撐正式的內部服務不在明示範圍內,得先去談。

三個官方入口,給了三種答案。既然如此,就只能查到底。

https://ithelp.ithome.com.tw/upload/images/20260903/2018355078hBhvS3t6.png
圖 1:前三格來自 Jina 的 repo、第 ④ 格來自 Qwen 的底模 repo——都是官方來源,卻拼不出一致的機器可讀答案。

授權不是包裝的人選的,是底模傳下來的

第 ④ 格才是根因:底模 Qwen2.5-VL-3B-Instruct 的 license 欄同樣是空的,寫著 qwen-research 的是 license_name 這個更少人看的欄位。而且 jina-v4 連 base_model 都沒填,底模只在 model card 正文裡。沒有哪個機器可讀的欄位永遠靠得住。

通則是衍生模型的授權下限由底模決定。要講精確一點:在沒有另外向上游取得授權的前提下,底模給的是下限——上面的人可以再收緊,不能放寬。

同一家 jina 兩個方向都有:jina-embeddings-v5-text-small 的底模是 Apache-2.0,自己卻標 cc-by-nc-4.0(在文本上加限制,沒問題);jina-embeddings-v4-mlx-8bitbase_model 就寫著 v4,卻標 apache-2.0,比底模寬。比底模寬鬆的標示先當成標錯——量化版與移植版一樣是衍生。

那個前提不是空話。有人在 jina-v4 的 repo 問過,jina 的人回:他們確實聯絡了 Qwen 團隊、拿到發布同意,但「we got an agreement to publish the model, we don't have a license for its commercial use」【官方,HF discussions #44】。上游授權談得到一部分,沒談到的那部分還綁著。

https://ithelp.ithome.com.tw/upload/images/20260903/20183550fGghE4EpFh.png
圖 2:會被自動化掃描器放行的,正好是有問題的那兩條——它們最上面那一跳寫的都是 MIT。

23 顆裡有 20 顆找不到 LICENSE 檔,所以它不能當唯一入口——那些 repo 的 tag 反而是僅有的機器可讀證據。

所以動作是四個地方都拉一次,對不上往嚴的靠:tag、license_name、model card 的 License 段、LICENSE 檔,再沿底模做一次——這是上線前的風險控制,不是法律解釋。

放在地端,並不代表條款就此失效。 jina 在 v3 的 model card 上,把「on-premises within your company」直接算進 CC BY-NC 的管轄範圍。而 QRL 把 research/evaluation 列在明示許可裡——你今天要做的事落在其中。它卡的不是評估,是上線。算不算商用,最後是法務說了算。

順序倒過來的理由就在這:等你建完索引、跑完評估才撞上「要重談授權」,換一個向量空間就是全量重新編碼加重建索引。(縮維度是截斷、不必重跑模型,但向量仍要重寫、index 仍要重建。)

先別談第一名。連授權都過不了,分數就沒有討論的價值。

所以我的順序是:授權 → max length → 語言 → 尺寸 → 分數。

第二關:「吃得下幾個 token」有四個欄位在回答

multilingual-e5-large 的 model card 白紙黑字寫著「Long texts will be truncated to at most 512 tokens」。但散文寫不進 CI,你要的是欄位。

https://ithelp.ithome.com.tw/upload/images/20260903/20183550TVeWEudUb4.png
圖 3:四欄依序是 config.jsonmax_position_embeddingssentence_bert_config.jsonmax_seq_lengthtokenizer_config.jsonmodel_max_length、model card 宣告值。一致的三顆都是雙向 encoder(Granite 還是 2026 年的),打架的四顆全是從 LLM 改出來的——不是新舊的問題,是出身的問題。

前兩顆的 514 對 512、8194 對 8192 只差 2,那是 RoBERTa 系把位置從 pad_token_id + 1 起算,多出來兩格不能用,不是矛盾。

要小心的是 bge-code-v1sentence_bert_config.json 寫 32,768,tokenizer_config.jsonmodel_max_length 卻是 256。走 sentence-transformers 讀前者、切在 32,768;自己拿 AutoTokenizertruncation=True 吃的是後者、切在 256。同一顆模型、同一份文字,兩條路差 128 倍。

至少,報錯還算坦白。真正麻煩的,是它早已截斷,卻什麼也不說。sentence-transformers 或裸 tokenizer,它收到顯式的 max_lengthtruncation,後半段人間蒸發、不噴錯誤,只有 recall 悄悄少一截。走 Day 21 那套 vLLM 則相反:超長直接回錯誤,吵但安全。

⚠️ 但 vLLM 還有第三條路:enable_chunked_processing(預設 False)開了之後,超長輸入會被拆段、各自編碼再加權平均【官方,vLLM vllm/config/pooler.py】——不報錯了,但那不是模型一次讀完全文。那是另一個候選,不是把牆推開。

昨天那張表的 L3 驗證是 dense 與 BM25 分跑比 recall@50。截斷確實會讓 dense 難看,但你會把它記到「dense 對專有名詞爛」頭上。所以 L3 要多一條:印一次 max_seq_length,跟你的 chunk 長度比一比。

順帶收一條 Day 21 的線:那張預算表的 embedding 開 --max-model-len 1024,而規格上限是 32K——那個 1024 是顯存預算逼出來的。規格線靜靜切掉、預算線直接報錯、chunk 長度你自己給——三條擋你的方式不同,但都要取最小的那條。

第三關:語言——沒有一張榜能替你的繁中選

榜首之間常常沒有統計顯著差異。 Lyon NLP 團隊拿 MTEB 的 raw results 做過統計檢定:2024-03 那個時點,法文榜前 9 名在 p=0.05 下統計等價,建議別看平均分、要看你 use case 那幾個子任務【引用,huggingface.co/blog/lyon-nlp-group/mteb-leaderboard-best-practices,2024-03 快照】。那是 26 個任務的平均,英文榜有 56 個——題目越少越看不出顯著。

榜是可以刷的。 同一份文件明講 data leakage 與 overfitting 會污染評估。MTEB 的對策寫進了 codebase,leaderboard 據此提供 zero-shot 過濾,而它預設是 Allow All——看榜的第一個動作不是往下捲,是把它切成 Only Zero-shot

大模型也不一定贏。 MMTEB(500+ tasks、250+ 語言)的結論有兩半:數十億參數的 LLM 確實能在某些語言子集拿下 SOTA,但整體最佳的公開模型是 560M 的 multilingual-e5-large-instruct【引用,arXiv:2502.13595,v1 2025-02】。

中文要切到 C-MTEB。它不是另一個網站,是同一個 leaderboard 下拉選單裡的另一個 benchmark,今天叫 MTEB(cmn, v1)、31 個 task(C-Pack 論文當年是 6 tasks / 35 datasets【引用,arXiv:2309.07597】)。不切過去,你看的還是英文榜。

而繁中只以多語任務的 zho-Hant 子集零星存在;社群另有拿 DRCD 做的繁中評測,所以不是完全沒得跑——但那些語料都不是機械手冊、料號與台灣產業術語。這一關沒有現成答案可查,只能自建題庫。

第四關:尺寸——你的卡塞不塞得下

過了前三關,才輪到「哪一顆」。這張不是排名,是過關之後我會先放進題庫的名單;最後一欄只回答「權重載不載得進」,不代表整套 workload 跑得動。

角色 模型 維度 / 有效上限 授權(括號是底模) 16-bit 權重 GB 載得進 12 GiB
現役候選 · 中文多語 ibm-granite/granite-embedding-311m-multilingual-r2 768 / 32K Apache-2.0 0.62
現役候選 · 中文多語(1B 級) nvidia/Nemotron-3-Embed-1B-BF16 2048 / 32K OpenMDW 1.1,不是 Apache(底模 Ministral-3-3B 反而是 Apache-2.0) 2.28
現役候選 · 程式碼 BAAI/bge-code-v1 1536 / 32K(⚠️ tokenizer 256) Apache-2.0 3.09
現役候選 · 表格與掃描件 Qwen/Qwen3-VL-Embedding-2B 2048 / 32K Apache-2.0(Qwen3-VL-2B-Instruct 同) 4.26
成熟基準 · 2024 世代 BAAI/bge-m3 1024 / 8192 MIT 1.14
成熟基準 · chunk 短 intfloat/multilingual-e5-large 1024 / 512 MIT 1.12
系列對照 · Day 21 那格 Qwen/Qwen3-Embedding-0.6B 1024 / 32K Apache-2.0(Qwen3-0.6B-Base 同) 1.19
授權案例,不是推薦 jina-embeddings-v4 2048 / 32K QRL 非商用(Qwen2.5-VL-3B-Instruct 傳染) 7.51 + 0.36 塞得下,卡在授權

讀表三條:

  • 有效上限取官方宣告值,不取 config.json 最大的那個數——後者對表上八顆有五顆更大。
  • 權重是「實際參數量 × 2 bytes」的【推算】、十進位 GB(fp16 與 bf16 都是 2 bytes);卡容量是二進位 GiB,12 GiB = 12.88 GB。
  • 例外與逐顆 commit SHA 在 research/day23/NOTES.md §6。

分組不是排版,是判準。下面三列留下的理由各不相同:BGE-M3 與 multilingual-e5-large 是成熟基準,生態最厚但權重停在 2024;Qwen3-Embedding-0.6B 留著是為了接回 Day 21 的顯存帳。三顆都不是這張表的優先候選,而 jina-v4 更只是案例。

心算一條就夠:實際參數量 × 2 bytes = 16-bit 權重(Day 05 那本 bpw 帳的極簡版)。重點在「實際」——名字裡的 1B、2B 是四捨五入後的行銷名,表上那顆 Nemotron 實抓 1.14B,2.28 GB 而不是 2.00。至於 checkpoint 出貨用什麼 dtype 與逐顆參數量,收在 NOTES。

這一欄也不是全部:non-KV runtime 與落在 util 預算之外的 CUDA context 都還沒進帳,Day 21 算過那兩欄。

權重載得進顯卡,只代表它有資格上場。能不能留下,還得回到 Day 21 的整卡預算。

BGE-M3 還留著,是因為它一顆同時輸出 dense、sparse 與 ColBERT 三種表徵(後兩路要用官方 FlagEmbedding),而且是 encoder、不需要跨 step 的 KV cache。另外,單向量與 late interaction 是兩條路:sentence-transformers 6.0 起把 multi-vector 收成一等公民,細節匹配更強,代價是索引大得多。

⚠️ 而且別為了長 chunk 去換 Qwen3-Embedding-0.6B:它是 decoder,一條 32K 序列的 KV 要 3.50 GiB(2 × 28 層 × 8 kv heads × 128 × 2 bytes × 32,768),是 Day 21 那格 0.31 GiB 的十一倍——在那張三張嘴的卡上,32K 是規格不是預算。換它的理由是授權:Apache-2.0 有明示的專利授權條款,MIT 沒有。

第五關:分數——而且它比你以為的鈍

走到這裡候選通常只剩兩三顆,這時才用你自己的題庫分勝負。量的是 Hit@10:這一題的正確 chunk 有沒有任何一個進前十,中記 1、沒中記 0。

它跟昨天那道 recall 階梯不是同一個數:recall@k 的分母是「這題有幾個正確 chunk」,Hit@10 只問有沒有——配對檢定要的就是這種二元結果。

而這把尺很粗。同一組題、配對比較(exact McNemar,雙尾 α=0.05):

兩顆的真實差距 50 題的檢定力 200 題 80% 要幾題
5 pp 6.9% 37.3% 498
10 pp 24.1% 87.5% 168
20 pp 69.9% ~100% 61

【推算,exact McNemar,假設不一致對比例 15 / 20 / 30%;算式在 research/verify-day23-numbers.py

所以 golden set 不是拿來解析零點幾分的,那個解析度它沒有。而且連「一大截」都比想像中吃力:50 題對 20 pp 只有約七成機會驗出來;要把檢定力拉到 80%,20 pp 要 61 題、10 pp 要 168 題、5 pp 要 498 題。

名次換得多快?gte-Qwen2-7B 自稱 2024-06-16 中英榜都第一,Qwen 則在 2025-06-05 說 Qwen3-Embedding-8B 拿下 MTEB multilingual 第一——不同榜,但間隔將近一年。今天誰第一我沒有一手證據:leaderboard 是 Gradio space,抓下來只有空殼。

榜會換,tag 會改,LICENSE 甚至也改過。既然答案會變,就別背答案。把查法留下來。

今天的實驗需要什麼

前兩支只查 metadata,一台能上網的機器就夠、不下載權重;第三支會下載三顆模型並對你的語料做推論,CPU 跑得動,但實務上會想要一張卡。三支的完整可執行版都在 research/day23/,正文只留判斷方式。

# 1) 授權入口盤點: 四個欄位一起看, 印完把 base 餵回去再跑, 直到沒有底模
#    只找入口, 不判讀條款 —— LICENSE 原文仍要自己讀完
curl -s "https://huggingface.co/api/models/jinaai/jina-embeddings-v4" | python3 -c "
import sys, json
d = json.load(sys.stdin); c = d.get('cardData') or {}
print(c.get('license'), '|', c.get('license_name'), '| @', d['sha'][:7],
      '|', [x['rfilename'] for x in d['siblings'] if 'LICEN' in x['rfilename'].upper()],
      '|', c.get('base_model'))"

# None | None | @ 853c867 | ['LICENSE'] | None
#   四個欄位, 三個是 None —— 連底模都要自己去 model card 正文撈

這支只是看一眼;掃過 23 顆、含缺欄保護與 tags 裡另一個 base_model 藏身處的完整版是 license_probe.py

# 2) 有效上限體檢: 先抽查 tokenizer 這一格, 不必載模型
#    三個檔不會互相同意, 而且還不是全部 —— 官方 card 有時比三個都小
curl -sfL "https://huggingface.co/BAAI/bge-code-v1/resolve/main/tokenizer_config.json" \
| python3 -c "import sys,json;print([f'{k}={v}' for k,v in json.load(sys.stdin).items() if 'max' in k])"
# ['model_max_length=256']    <- sentence_bert_config 與 config 各拉一次, 那兩個都是 32768

第三支要你自己的資料:把昨天那份 golden.jsonl 補到 50 題以上,欄位沿用不變——別另開一份,Day 24 一動 chunk size,id 就整批作廢。

名單是兩顆現役候選加一顆成熟基準;表格與掃描件那顆走多模態,題庫不一樣,不塞進同一組。

# 3) 比 Hit@10 + 配對 exact 檢定 (含載入與清理的完整版: research/day23/hit_at_k.py)
from math import comb               # texts/ids 來自 chunks.jsonl, gold 來自昨天那份 golden.jsonl
K = 10; assert len(gold) >= 50 and len(texts) >= K   # 檢定力由題數決定, 不是 chunk 數

CAND = [   # 名稱, doc 端前綴, query 端的官方用法 —— 前綴不是裝飾, 少了會系統性低估
    ("ibm-granite/granite-embedding-311m-multilingual-r2", "",          {}),
    ("nvidia/Nemotron-3-Embed-1B-BF16",                    "passage: ", {"prompt": "query: "}),
    ("BAAI/bge-m3",                                        "",          {}),   # 成熟基準
]
hits = {}
for name, dp, qkw in CAND:
    m = SentenceTransformer(name)
    D = m.encode([dp + t for t in texts], normalize_embeddings=True)
    Q = m.encode([g["question"] for g in gold], normalize_embeddings=True, **qkw)
    top = np.argpartition(-(Q @ D.T), K - 1, axis=1)[:, :K]
    hits[name] = np.array([bool(set(g["gold_doc_ids"]) & {ids[i] for i in row})   # 交集, 不是相等
                           for g, row in zip(gold, top)])
    print(name, f"Hit@{K} =", round(hits[name].mean(), 3))

def exact_p(b, c):                  # 只看兩顆結果不一致的那些題, 雙尾
    n = b + c
    return 1.0 if n == 0 else min(1.0, 2 * sum(comb(n, i) for i in range(min(b, c) + 1)) / 2 ** n)

for i, (x, *_) in enumerate(CAND):
    for y, *_ in CAND[i + 1:]:
        a, b = hits[x], hits[y]
        na, nb = int((a & ~b).sum()), int((~a & b).sum())
        print(x, "vs", y, na, nb, f"exact p = {exact_p(na, nb):.4f}")

兩兩比較那組才是重點:p 值大不代表兩顆一樣好,只代表題數還分不出來

這一篇不附我的 Hit@10 數字:它由你的語料決定,附上去只會邀請你拿我的絕對值去對你的系統。要帶走的是那張檢定力表。

小結

  • 選 embedding 的順序是授權 → max length → 語言 → 尺寸 → 分數:最貴的關排最前面。
  • 授權看四個地方,底模給的是下限:沒另談上游授權時只能收緊、不能放寬;沒有單一欄位靠得住,對不上往嚴的靠。
  • 「吃得下幾個 token」有四個欄位在回答,最多差 128 倍;再跟 chunk 長度與服務端上限一起取最小,而且要知道超長之後這條路是靜默截斷還是報錯。
  • 最後一關本來就鈍:50 題只夠當早期 smoke test,連 20 pp 都要 61 題才有 80% 把握——所以前四關才要嚴。

明天 Day 24〈一份 80MB 的 PDF,chunk 要切幾刀?英文有答案,中文我來補〉:今天量的是模型那端吃得下多長,明天從文件那端切過來,看中文到底該切幾刀。

那麼,明天見。


上一篇
🕵 Day 22 - 答錯不是模型笨:RAG 準確度的七層歸因
下一篇
Day 24 - 我替中文 chunk 補了標點,句尾命中率反而變成 0%
系列文
不是模型太慢,是你沒算過這筆帳:地端 LLM 工程實戰 30 天25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言